package cn.tedu.charging.order.service.impl;

import cn.tedu.charging.order.common.ChargingConstants;
import cn.tedu.charging.order.common.MqttConstants;
import cn.tedu.charging.order.feign.DeviceClient;
import cn.tedu.charging.order.feign.UserClient;
import cn.tedu.charging.order.mqtt.MqttProducer;
import cn.tedu.charging.order.pojo.dto.ChargingDto;
import cn.tedu.charging.order.pojo.param.OrderAddParam;
import cn.tedu.charging.order.quartz.SchedulerExecutor;
import cn.tedu.common.pojo.po.DeviceMQPO;
import cn.tedu.charging.order.pojo.po.OrderMQPO;
import cn.tedu.charging.order.rabbit.RabbitMQProducer;
import cn.tedu.charging.order.service.OrderService;
import cn.tedu.common.pojo.JsonResult;
import cn.tedu.common.pojo.param.GunStatusUpdateParam;
import cn.tedu.common.pojo.utils.JsonUtils;
import cn.tedu.common.pojo.vo.StationInfoVO;
import cn.tedu.common.pojo.vo.UserInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Random;


@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
    /**
     * 创建订单
     *
     * 1 生成订单号  id生成
     * 2 获取用户信息 open-feign 用户服务
     * 3 获取场站信息 open-feign 设备服务
     * 4 修改枪的状态 open-feign 设备服务
     * 5 发送开始充电指令  EMQX(物联网消息中间件) ，设备从EMQX获取数据
     * 6 发送消息解决超时订单 RabbitMQ
     * 7 定时检测设备状态
     * @param orderAddParam
     * @return
     */
    @Override
    public String createOrder(OrderAddParam orderAddParam) {
        //1 生成订单号
        String orderNo = getOrderNo();

        //2 获取用户信息 车辆id
        // 如果获取失败 应该交给全局异常处理给用户提示
        log.debug("获取用户信息:{}",orderAddParam.getUserId());
        UserInfoVO userInfo = getUserInfo(orderAddParam.getUserId());
        log.debug("获取用户信息:{},结果:{}",orderAddParam.getUserId(),userInfo);

        //3 获取场站信息 场站id
        // 如果获取失败 应该交给全局异常处理给用户提示
        log.debug("获取场站信息:{}",orderAddParam.getGunId());
        StationInfoVO stationInfo = getStationInfo(orderAddParam.getGunId());
        log.debug("获取场站信息:{},结果:{}",orderAddParam.getGunId(),stationInfo);

        //4 修改枪的状态
        // 如果获取失败 应该交给全局异常处理给用户提示

        log.debug("通过Feign修改枪的状态:{}",orderAddParam.getGunId());
        long feignStartTime = System.currentTimeMillis();
        Boolean success = updateGunStatus(orderAddParam.getGunId());
        long feignEndTime = System.currentTimeMillis();
        log.debug("通过Feign修改枪的状态:{},结果:{},耗时:{}",orderAddParam.getGunId(),success,feignEndTime - feignStartTime);

        // 修改枪的状态的结果 不会影响下面 给 设备发送开始充电指令 的业务
        // 所以可以通过 RabbitMQ 和 异步 设备进行通信
        // 高并发下 异步可以提升性能 无需等待 通过 Feign 调用设备更新枪状态接口的返回结果
        // 只有保证消息成功发送到 MQ 就可以了

        log.debug("通过MQ修改枪的状态:{}",orderAddParam.getGunId());
        long mqStartTime = System.currentTimeMillis();
        Boolean mqSuccess = sendDeviceMessage(orderAddParam.getGunId(),ChargingConstants.GUN_STATUS_BUSY);
        long mqEndTime = System.currentTimeMillis();
        log.debug("通过MQ修改枪的状态:{},结果:{},耗时:{}",orderAddParam.getGunId(),mqSuccess,mqEndTime - mqStartTime);

        //5 发送开始充电指令
        startCharging(orderNo,userInfo.getCarId(),
                stationInfo.getOperatorId(),
                stationInfo.getStationId(),orderAddParam.getPileId(), orderAddParam.getGunId());
        //6 进行设备自检(检查订单是否正常，记录的是成功订单还是失败订单)
        try {
            SchedulerExecutor.scheduleCheckJob(orderNo, orderAddParam.getGunId());
        }catch (Exception e){
            log.error("设备自检失败:{}",e.getMessage());
        }
        //7 给RabbitMQ发送消息 用来处理因为设备故障导致超时订单
        sendOrderMessage(orderNo);

        return orderNo;
    }

    @Autowired
    private RabbitMQProducer rabbitMQProducer;

    private void sendOrderMessage(String orderNo){
        OrderMQPO orderMQPO = new OrderMQPO();
        orderMQPO.setOrderNo(orderNo);
        rabbitMQProducer.send(orderMQPO);
    }

    /**
     * 给设备发送消息 更新枪的状态
     * @param gunId
     */
    private Boolean sendDeviceMessage(Integer gunId,Integer status){
        //省去 创建PO 直接传 gunId
        Boolean success = false;
        try {
            DeviceMQPO deviceMQPO = new DeviceMQPO();
            deviceMQPO.setGunId(gunId);
            deviceMQPO.setStatus(status);
            rabbitMQProducer.sendToDevice(deviceMQPO);
            success = true;
        }catch (Exception e) {
            log.error("设备发送消息失败:{}",gunId);
        }
        return success;
    }

    @Autowired
    private DeviceClient deviceClient;

    /**
     * 修改枪的状态
     * @param gunId
     * @return
     */
    private Boolean updateGunStatus(Integer gunId) {
        GunStatusUpdateParam param = new GunStatusUpdateParam();
        param.setGunId(gunId);
        //param.setStatus(1); //魔数 魔法数字 应该用常量或者枚举管理起来
        param.setStatus(ChargingConstants.GUN_STATUS_BUSY);
        JsonResult<Boolean> result = deviceClient.updateGunStatus(param);
        if (result != null) {
            return result.getData();
        }
        return false;
    }

    /**
     * 获取场站信息
     * @param gunId
     * @return
     */
    private StationInfoVO getStationInfo(Integer gunId) {
        JsonResult<StationInfoVO> stationInfo =
                deviceClient.getStationInfo(gunId);
        if (stationInfo != null) {
            return stationInfo.getData();
        }
        return null;
    }

    @Autowired
    private UserClient userClient;

    /**
     * 获取用户信息
     * @param userId
     * @return
     */
    private UserInfoVO getUserInfo(Integer userId){
        JsonResult<UserInfoVO> userInfo =
                userClient.getUserInfo(userId);
        if (userInfo != null) {
            return userInfo.getData();
        }
        return null;
    }

    /**
     * 生成订单号
     * 订单号的要求
     *
     * 全局唯一性：不能出现重复的ID号，既然是唯一标识，这是最基本的要求。
     * 趋势递增：在MySQL InnoDB引擎中使用的是聚集索引，由于多数RDBMS使用B-tree的数据结构来存储索引数据，在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
     * 单调递增：保证下一个ID一定大于上一个ID，例如事务版本号、IM增量消息、排序等特殊需求。
     * 信息安全：如果ID是连续的，恶意用户的扒取工作就非常容易做了，直接按照顺序下载指定URL即可；如果是订单号就更危险了，竞对可以直接知道我们一天的单量。所以在一些应用场景下，会需要ID无规则、不规则
     *
     * 订单号生成规则
     * Java中的UUID  随机数  UUID能不能到数据库的主键 ？能 不推荐
     *
     * 分布式id生成器  分布式场景下能 高性能的 生成 唯一 尽量无规则 的id
     * 雪花算法 https://www.jianshu.com/p/1af94260664a
     * 美团 leaf  https://tech.meituan.com/2017/04/21/mt-leaf.html
     * 时间 + 设备信息（桩id、枪id） + 随机数
     *
     * 现在业务刚开始 先自己生成
     * 10000 + 随机数 + 时间戳
     *
     *
     * @return
     */
    private String getOrderNo(){
        String start = "10000";
        Random random = new Random();
        int r = random.nextInt(10000);
        //获取当前的时间戳
        long now = System.currentTimeMillis();
        //Date date = new Date();
        //LocalDateTime 是 Date 的替代 更推荐使用 LocalDateTime
        //LocalDateTime now = LocalDateTime.now();
        //StringJoiner stringJoiner = new StringJoiner();
        // 不是线程安全
        /*StringBuilder sb = new StringBuilder();
        sb.append(start);
        sb.append("_");
        sb.append(r);
        sb.append("_");
        sb.append(now);*/

        //synchronized 线程安全
        /*StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(start);
        stringBuffer.append("_");
        stringBuffer.append(r);
        stringBuffer.append("_");
        stringBuffer.append(now);*/
        String result = start + "_" + r + "_" + now;
        return result;
    }

    @Autowired
    private MqttProducer mqttProducer;

    /**
     * 给设备发送开始充电指令
     * @param orderNo
     * @param carId
     * @param stationId
     * @param pileId
     * @param gunId
     */
    public void startCharging(String orderNo,Integer carId,
                              Integer operatorId,
                              Integer stationId,
                              Integer pileId,Integer gunId){
        //1 Topic设计
        // 一种是枪的维度   或者 一种是桩的维度 消息体包含枪的信息
        // 从业务上讲都可以, 如果是枪的维度 topic的数量会比 桩的维度 的数量多
        // 占用更多的资源
        // 所以为了节省资源 采用 桩的维度
        String topic = MqttConstants.TOPIC_START_CHARGING_PREFIX + pileId;
        //2 消息设计
        //类似于设计接口的入参，思考业务 如果少了哪个入参，业务无法完成，这些入参就是核心参数
        //首先明确业务目标， 给设备发送开始充电指令给一个用户的订单开始充电
        //参数  订单信息的唯一标识 订单id 设备信息  桩号，枪号 告诉具体是哪个设备 开始充电
        ChargingDto chargingDto = new ChargingDto();
        chargingDto.setOrderNo(orderNo);
        //设置运营商id
        chargingDto.setOperatorId(operatorId);
        chargingDto.setStationId(stationId);
        chargingDto.setPileId(pileId);
        chargingDto.setGunId(gunId);
        chargingDto.setCarId(carId);
        chargingDto.setMsg(ChargingConstants.START_CHARGING);
        log.info("订单服务发送开始充电指令到设备 topic:{},msg:{}",topic,chargingDto);
        //3 消息序列化
        //String string = chargingDto.toString();
        //消息体的序列化 Spring-Boot 自动做的序列化 对象转json ,json转对象
        //需要我们手动序列化,不能 toString ,应该把 对象 chargingDto 转换为JSON
        //Jackson 用来序列化和反序列化 json 的 Java 的开源框架, Spring-Boot 怎么做的，我们可以参考借鉴
        String message = JsonUtils.toJson(chargingDto);
        Boolean success = mqttProducer.send(topic, message);
        log.debug("订单服务发送开始充电指令到设备 topic:{},msg:{},结果:{}",topic,message,success);
    }
}
